Repository navigation
ci(deps): bump dawidd6/action-download-artifact from 25 to 27 - #53
Merged
eldonm merged 97 commits intoOct 11, 2026
Merged
Conversation
* feat(db): add PostgreSQL backend with serverless-tuned pooling
PostgresDB joins JsonDB/SQLite/MongoDB/DynamoDB as a first-class
backend. Targets Neon, Aurora Serverless v2, and self-hosted PG 15+.
- jvspatial/db/postgres.py — asyncpg pool with auto-tuned sizing
(Lambda: 0/3, long-running: 2/10), session/transaction pooler modes,
schema-scoped tables, JSONB storage, vector column support, native
$near peeling, multi-tenant RLS via current_tenant contextvar,
transactional save/get/find/delete via PostgresTransaction, and
drop_deprecated_indexes for migrations.
- jvspatial/db/_postgres_translate.py — Mongo-style query → SQL
translator with positional-placeholder builder.
- jvspatial/db/factory.py — register "postgres"/"postgresql" with
env-driven defaults (DSN, pool sizes, pooler mode).
- jvspatial/db/transaction.py — backend-agnostic transaction_context.
- docker/ — local dev pgvector container + .env example.
- docs/md/postgres-guide.md, neon-deployment.md,
aurora-serverless-deployment.md, multi-tenant-rls.md,
cursor-pagination.md, schema-migrations.md, vector-store.md.
- tests/db/test_postgres_{unit,integration,translate}.py,
tests/db/test_cursor_pagination.py.
Companion changes pulled in:
- jvspatial/core/migrations.py + tests — record-level schema migrations.
- jvspatial/core/validate.py + tests — graph-shape validation.
- jvspatial/core/entities/walker_components/trail_store.py + tests —
pluggable walker trail persistence.
- jvspatial/api/auth/_session_store.py + tests — pluggable auth session
store (in-process / Redis) for multi-worker deployments.
- jvspatial/observability/tracing.py + tests — OTel span helpers.
- jvspatial/cli.py — `jvspatial migrate` CLI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(spec): reusable OAuth 2.1 service design (M1)
Authlib-based OAuth 2.1 AS+RS layered on jvspatial's existing auth.
RS256+JWKS access tokens, DCR, PRM/AS metadata, audience binding.
Foundation for OAuth-secured MCP (M2 = Integral MCP endpoint, M3 = UI).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(plan): M1a OAuth foundation implementation plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(oauth): add cryptography dependency for RS256
* feat(oauth): OAuthClient + AuthorizationCode storage models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): RS256 signing-key store + JWKS
Add OAuthSigningKey Object model and keys.py module: RSA-2048
keypair generation, persist/load via Object.find, idempotent
ensure_signing_key(), and JWKS builder (public keys only). All
three TDD tests pass. PyJWT 2.x to_jwk requires cryptography key
object; PEM loaded via serialization.load_pem_public_key before
conversion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): AuthConfig OAuth fields (off by default)
Add oauth_enabled, oauth_issuer_url, oauth_prefix, oauth_supported_scopes,
oauth_dcr_enabled, oauth_access_token_ttl_minutes, oauth_code_ttl_seconds,
and accept_oauth_bearer to AuthConfig. All default off/empty so existing
apps are unaffected. Covered by two-test TDD suite in
tests/api/auth/oauth/test_oauth_authconfig.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(spec): M1b design addendum — Authlib AS integration + jvspatial wiring map
* docs(plan): M1b-1 OAuth core authorization server plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(oauth): add authlib dependency
* feat(oauth): anyio sync/async bridge + Starlette OAuth2Request wrapper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): ClientMixin adapter over OAuthClient
Implements OAuthClientAdapter wrapping OAuthClient so Authlib can
validate redirect URIs, grant/response types, scope filtering, and
token endpoint auth methods (public PKCE + confidential secret check).
All three tests (redirect/grant checks, public auth method + scope
filter, confidential secret check) pass; black/isort/flake8/mypy clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): authorization-code+PKCE server issuing RS256 tokens (anyio-bridged)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(oauth): enforce mandatory S256 PKCE for all clients (security)
Adds RequiredS256CodeChallenge subclass that overrides the authorize-step
hook to reject missing code_challenge (regardless of client auth method)
and reject code_challenge_method=plain. Registers it in
build_authorization_server instead of CodeChallenge(required=True), which
only enforced PKCE for public clients (auth_method=="none").
Two regression tests added: confidential client without PKCE is rejected,
and plain code_challenge_method is rejected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(plan): M1b-2 refresh grant + scope-intersection + token hardening
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): OAuthRefreshToken model + refresh store
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): issue + persist refresh tokens on auth-code flow
Wire _generate_refresh_token into JvSpatialJWTTokenGenerator so Authlib
emits an opaque rt_ token when client.check_grant_type("refresh_token")
is true; implement save_token to persist the plaintext (hashed) via
refresh_store.mint_refresh_token through a positional async shim
(_persist_refresh) compatible with call_async's positional-only contract.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): refresh-token grant with rotation + revocation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(oauth): scope-permission intersection + nbf claim + atomic single-use code
Three hardening fixes from the M1b-2 spec §6:
1. scope∩permissions — `save_authorization_code` now reads `permissions`
from `request.user` (when the grant_user dict carries it) and stores
only the intersection of the client-allowed scope and the user's
permissions in `AuthorizationCode.scope`. Back-compat: callers that
pass `{"id":...}` without `permissions` keep the full client scope.
Because `create_token_response` reads scope from
`authorization_code.get_scope()`, the intersected scope propagates
to the token `scope` claim automatically with no generator changes.
2. nbf claim — `JvSpatialJWTTokenGenerator.get_extra_claims` overrides
the Authlib base (which returns `{}`) to inject `"nbf": int(time.time())`.
`access_token_generator` merges extra claims via `token_data.update(...)`,
so `nbf` lands in every issued JWT. super() is called first so any
future upstream additions are preserved.
3. atomic single-use — `query_authorization_code` calls `_consume_code`
(sets `consumed=True` + saves) *before* returning the `StoredAuthCode`
to the grant. A concurrent duplicate exchange sees `consumed=True` on
the first load and is rejected. `delete_authorization_code` remains
idempotent (Authlib still calls it after token issuance; the record is
already consumed at that point, which is harmless).
All 9 server-flow tests pass (incl. 2 new: scope-intersection+nbf,
back-compat no-permissions); all 24 OAuth tests pass.
* fix(oauth): consume auth code only after full validation (no retry lockout)
Move single-use enforcement from query_authorization_code (runs before
redirect_uri + PKCE validation) to delete_authorization_code (runs only
after all validation passes on the success path, per authlib 1.7.2
authorization_code.py:create_token_response). A request with a wrong
code_verifier or wrong redirect_uri previously burned the code, locking
out the legitimate client's retry. Now query_authorization_code is
read-only; delete_authorization_code is the single-use point.
Also corrects the atomicity docstring: single-use is serialized by the
single-process anyio bridge, not a DB-level CAS; multi-worker deployment
will require a conditional-update primitive (tracked for a later phase).
Adds regression test test_failed_verifier_does_not_burn_code_for_retry
proving a failed verifier does NOT lock out a correct retry, and confirms
the existing single-use replay test still rejects a replayed code.
* docs(plan): M1b-3a DCR + revocation + reuse-detection + metadata
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): refresh-token reuse detection (family revocation)
Implements OAuth 2.1 §rotation family kill (review finding I-3).
When a rotated/revoked refresh token is replayed, the entire rotation
family is immediately revoked (including the live child token), and the
request is rejected. Normal rotation is unaffected.
- OAuthRefreshToken: +family_id field (shared across rotations of one grant)
- refresh_store: mint_refresh_token gains family_id param; add find_any
(lookup without is_active filter for replay detection) and revoke_family
(kill all tokens in a family)
- server.py: authenticate_refresh_token stashes rec.family_id on
request._oauth_family_id; save_token forwards it to _persist_refresh so
the rotated token inherits the family; fresh uuid4 family_id generated at
first mint (auth-code path). Replay of revoked token triggers revoke_family.
* feat(oauth): dynamic client registration (RFC 7591)
Implements JvSpatialClientRegistrationEndpoint (open registration,
MCP zero-config) + async_register_client on the authorization server.
JSON body wired via BasicOAuth2Payload on request.payload before
dispatching; public clients (auth_method=none) receive no client_secret
in the 201 response and store no secret hash.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): token revocation endpoint (RFC 7009)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): RFC 8414 AS metadata builder
Pure builder function ``build_as_metadata`` constructs the RFC 8414
Authorization Server metadata document (issuer, endpoints, PKCE,
grant types). Validated against ``authlib.oauth2.rfc8414.AuthorizationServerMetadata``
in the accompanying TDD test. Route to serve the document at
``/.well-known/oauth-authorization-server`` is deferred to M1b-3b.
* fix(oauth): DCR requires https redirect URIs (loopback http allowed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(plan): M1b-3b OAuth HTTP wiring (routes + mount + consent + rate-limit)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): mount OAuth routers + well-known metadata/jwks + startup key hook
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: OAuth program resume handoff (M1b-3b Task 1 done; next steps + known metadata-prefix bug)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(oauth): metadata advertises api-prefixed endpoints; DCR-over-HTTP test
build_as_metadata gains api_prefix kwarg (default "" for back-compat); the
well-known handler now passes APIRoutes.PREFIX so token/registration/revocation/
authorization endpoints are advertised as https://as.example/api/oauth/…
instead of missing the /api segment. register route pre-reads JSON body then
builds a StarletteOAuth2Request with empty form to avoid double-read error.
New test_oauth_http_flow.py covers both assertions over TestClient.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): /authorize consent + session-user permission resolution
Implement the real GET/POST /oauth/authorize flow, replacing the HTML
placeholder. GET renders a consent page (client name + requested scopes +
approve/deny form re-carrying the OAuth params); POST builds grant_user from
the bearer-authenticated session user ONLY — its id plus effective permissions
via get_effective_permissions(roles, permissions, role_permission_mapping) —
and calls async_create_authorization_response so the authorization server
intersects scope with those permissions. Deny redirects with
error=access_denied. Permissions are never read from client/request input,
making the scope∩permissions narrowing the trust boundary.
- Thread get_current_user from the auth configurator into build_oauth_routers
and Depends() it on both authorize routes (401 without bearer).
- Add JvSpatialAuthorizationServer.async_get_consent_grant to validate the
authorize request off-thread (anyio bridge) and surface the client/scope for
rendering; invalid requests yield a 400 page (no open redirect).
- Optional auth_config.oauth_consent_handler override; 503 when no
get_current_user is wired.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(oauth): rate-limit dynamic client registration (open-DCR abuse, I-1)
Add `oauth_dcr_rate_limit_per_minute` knob (default 12) to AuthConfig and
wire a tight rate-limit override onto `/api/oauth/register` at OAuth-configure
time — before `get_app()` builds the rate-limit middleware — so the MemoryRateLimitBackend
caps DCR bursts with a 429. When the server operator has not enabled rate limiting
globally, the flag is implicitly set to True so the override takes effect.
Existing auth and OAuth tests (42) remain green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(oauth): /authorize deny path validates redirect_uri (no open redirect)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(plan): M1c resource server (JWKS verifier + PRM + accept_oauth_bearer)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): resource-server access-token verifier
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): protected-resource metadata (RFC 9728) + 401 helper
Adds `build_prm()` and `www_authenticate_header()` to metadata.py.
Mounts `/.well-known/oauth-protected-resource` on `well_known_router`
(active only when `oauth_enabled=True`). Exempts the PRM path from the
bearer-auth middleware unconditionally so FastAPI returns a natural 404
when OAuth is disabled, matching the existing AS-metadata + JWKS pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(oauth): accept OAuth bearer tokens on protected endpoints (accept_oauth_bearer)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): emit WWW-Authenticate resource_metadata on 401 (RFC 9728 discovery)
When `accept_oauth_bearer=True`, the middleware now attaches
`WWW-Authenticate: Bearer resource_metadata="<issuer>/.well-known/oauth-protected-resource"`
to every 401 response (missing bearer AND failed-auth paths), so MCP
clients can auto-discover the Authorization Server per RFC 9728 §5.1 /
RFC 6750 §3. Header is absent when `accept_oauth_bearer=False` (no
change for non-OAuth servers). Import is lazy/guarded to avoid
coupling when the oauth subpackage is unavailable.
* docs: OAuth handoff — M1 complete (AS+RS), next = merge + M2/M3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): redirect unauthenticated GET /authorize to configured login SPA
When AuthConfig.oauth_authorize_login_redirect is set, an unauthenticated
GET /oauth/authorize now 302-redirects the browser to that SPA login URL
with the original OAuth query string appended, instead of returning the
legacy 401 JSON. This makes the OAuth authorization-code browser flow
completable. Empty (default) preserves the legacy 401 behavior.
The GET handler uses a non-raising optional-user seam (_optional_current_user)
that invokes get_current_user imperatively and converts its 401 to None, so
it can branch on the unauthenticated case. POST /authorize and the authed-GET
consent page are unchanged (still bearer-required). The redirect target is
ONLY the trusted config base + the original query — never a request-derived
host — so it cannot be turned into an open redirect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): supported-scope ceiling + empty-perms/wildcard scope hardening
Make the authorization-code scope grant secure-by-default once a scope
ceiling is declared, while staying a strict no-op for callers that do
not declare one.
- build_authorization_server / JvSpatialAuthorizationServer gain an
optional supported_scopes; stored as self._supported_scopes (defaults
to []). routes.py wires auth_config.oauth_supported_scopes through.
- save_authorization_code intersects the permission-narrowed scope
against the supported set when non-empty (empty => skip, back-compat).
- _intersect_scope: permissions is None => no narrowing (absent key,
back-compat); '*' in permissions => authorize all requested scope
(admin wildcard, fixes the inverted-admin footgun); else membership
filter. A present-but-empty [] now narrows to nothing.
- save_authorization_code no longer collapses []->None, so an explicit
empty permission set denies all scope.
BEHAVIOUR CHANGE (for CHANGELOG in M4-T6): a present-but-empty
request.user['permissions'] == [] now grants NO scope (was: full
requested scope). Absent key still preserves full scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): DCR filters requested scope against supported scopes
When the authorization server declares a supported-scope ceiling
(server._supported_scopes non-empty), the DCR endpoint now silently
intersects a client's requested scope against that set in
extract_client_metadata, so a client cannot self-register an unsupported
or elevated scope. Unsupported tokens are dropped (RFC 7591 permits the
AS to filter) rather than rejected, keeping zero-config MCP registration
working. Filtering in extract_client_metadata (not save_client) keeps a
single source of truth so both the persisted record and the echoed 201
scope reflect the filtered value.
Back-compat: an empty/undeclared ceiling is a no-op — requested scope is
persisted verbatim. This is defense-in-depth; the authorize-time ceiling
in JvSpatialAuthCodeGrant.save_authorization_code (M4-T1) remains the
primary bound on issued-token scope.
scopes_supported is deliberately NOT advertised in get_server_metadata:
Authlib's get_claims_options turns it into a hard issuperset validation
that would 400 an out-of-set scope before our silent filter runs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(oauth): atomic auth-code consume (CAS) to close multi-worker TOCTOU
Replace the blind `consumed = True; save()` in _consume_code with an
atomic compare-and-swap (find_one_and_update predicated on consumed=False),
mirroring the work_claim.py idiom. On a lost race (None result) the consume
raises InvalidGrantError — the same error a replayed/consumed code produces
via query_authorization_code — so the loser never gets a second token.
Consume still runs after PKCE/redirect validation (failed verifier does
not burn the code). Atomic on postgres (SELECT ... FOR UPDATE) and mongo
(native findOneAndUpdate); best-effort on sqlite/json (no regression vs
the prior blind write). Drop the consumed record from the GraphContext
cache so a stale cached entry can't let a replay slip through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ratelimit): IP-only identifier + first-class pluggable backend
7a — Drop the attacker-controlled User-Agent from the unauthenticated
rate-limit bucket key. The key is now the source IP alone (sha256-hashed):
folding in the UA let an attacker rotate the header per request to mint a
fresh bucket and evade the per-IP cap. Authenticated `user:{id}` path is
unchanged. (X-Forwarded-For trusted-proxy parsing remains deferred as 7b;
`request.client.host` stays as-is, documented as a known proxy limitation.)
3 — Declare `rate_limit_backend` as a real Optional[RateLimitBackend] field
on ServerConfig (default None -> process-local MemoryRateLimitBackend, behaviour
unchanged), replacing the prior getattr lookup. ServerConfig now allows
arbitrary types so the Protocol-typed backend can be stored. Documents the
multi-worker limitation (in-memory counter is per-process, so effective cap is
N x configured under N workers — including the DCR /oauth/register cap) on the
field, the server_configurator selection site, and _wire_dcr_rate_limit.
Tests: UA-independence of the identifier + same-IP shared bucket (no UA-rotation
evasion); rate_limit_backend field declared/defaults-None/accepts-instance and
server_configurator consumes a supplied backend (falls back to Memory otherwise).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(oauth): jti denylist for access-token revocation (RFC 7009)
Access tokens are stateless RS256 JWTs that previously stayed valid until
their exp regardless of /oauth/revoke. Add a per-token jti denylist so a
holder can revoke a specific access token before expiry:
- OAuthRevokedToken Object (jti + self-expiring expires_at = token exp)
- denylist.py store: revoke_jti (idempotent) / is_jti_revoked
- resource.verify_oauth_access_token: require jti + reject denylisted jtis
(Authlib's RFC-9068 generator always stamps jti; verified by decoding a
freshly-minted token). One indexed lookup per RS validation (hot-path note;
in-proc cache is a future optimization).
- revocation endpoint: when an access-token JWT is presented (or
token_type_hint=access_token), validate it via the same verify path and
denylist its jti. Refresh-token revocation unchanged. Still requires
client-auth + token possession (check_client on the client_id claim), so a
caller can only revoke a token they hold.
Per-token only; mass revocation (per-(user,client) revoked-after watermark)
is a separate future item since stateless jtis can't be enumerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(oauth): M4 hardening completion record + holistic security review
Records the five M4 fixes (scope ceiling, DCR scope filter, auth-code CAS,
IP-only rate-limit key + pluggable backend, jti denylist), the two intentional
behavior changes (permissions:[] => deny scope; IP-only rate-limit key), the
holistic adversarial review verdict (no over-grant on any branch, no fail-open,
back-compat preserved, no cross-cutting bypass), and the two documented
deferrals (Item 5 RFC-8707 per-request resource->aud; Item 7b trusted-XFF).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(oauth): M4 record — cross-repo verification outcome (integral consent ceiling fix 1bb5f5a)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: keep jvspatial implementation-agnostic — generalize integral references
jvspatial is a generic application framework; remove the two remaining
authoritative references to a specific downstream app (integral):
- db/postgres.py: partial-filter docstring now says 'jvspatial and its
downstream applications' instead of naming jvagent/integral
- docs/md/multi-tenant-rls.md: RLS is 'a load-bearing isolation primitive
for any multi-tenant platform built on it', not 'the Integral platform'
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(oauth): use generic app.example.com fixtures (no app-specific names in jvspatial)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(oauth): add M4 hardening implementation plan to .planning (companion to the COMPLETE record)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: enable pretty printing for JSON serialization across both orjson and standard json backends
* chore(release): clean up packaging for 0.0.9 PyPI release
Make pyproject.toml the single source of build metadata and fix the
metadata/packaging issues found in the pre-release review.
Packaging:
- Remove setup.py; resolve version dynamically via
[tool.setuptools.dynamic] from jvspatial/version.py. Fixes a
dual-source conflict where the wheel published Requires-Python
>=3.8 (EOL) while the project targets >=3.9.
- Add the `cache` extra (redis[hiredis]) so `pip install
jvspatial[cache]` works; it backs jvspatial.cache.redis.
- Complete the `all` extra: lambda, postgres, pgvector, otel, cache,
scheduler.
- Remove the redundant top-level requirements*.txt files; deps live
solely in pyproject.toml extras.
CI / release:
- publish.yml: switch from API-token upload to PyPI Trusted
Publishing (OIDC) via pypa/gh-action-pypi-publish; add id-token:
write; drop dead setup.py/requirements config-change grep.
- security.yml: drop the now-inert requirements*.txt path filters.
Docs / templates:
- CHANGELOG: promote [Unreleased] to [0.0.9] - 2026-06-14 with a
Packaging section.
- bug_report.md: drop downstream Jaclang/Jivas fields; add OS, DB
backend, serverless mode.
- Update workflow docs (README/VERSIONING/WORKFLOW_SUMMARY),
RELEASING.md, and docs/md to drop setup.py/requirements*.txt
references.
Verified: clean sdist+wheel build, twine check PASSED, fresh-venv
install + import + CLI all green; wheel METADATA shows version 0.0.9
and Requires-Python >=3.9.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(security): stop unpickling untrusted Redis blobs in json mode; fix auth docs
Security:
- jvspatial/cache/redis.py: in the default `json` serialization mode the
deserializer fell through to pickle.loads on any unprefixed value, so
anyone able to write to the Redis keyspace could achieve RCE even with
the "safe" default. JSON mode now refuses non-JSON values (raised and
caught by get() as a cache miss -> recompute). Legacy pickle entries are
readable only via explicit opt-in (allow_legacy_pickle=True /
JVSPATIAL_REDIS_ALLOW_LEGACY_PICKLE=true) on a trusted keyspace.
Explicit `pickle` mode is unchanged.
Docs:
- README + docs/md/{authentication,api-keys,migration}.md used a flat
`auth_enabled=True` kwarg that ServerConfig silently ignores (auth
stayed disabled). Corrected to the nested `auth=dict(...)` form.
Cleanup:
- jvspatial/cache/layered.py: replace print() Redis-unavailable warnings
with logging.warning.
- .env.example: document JVSPATIAL_REDIS_ALLOW_LEGACY_PICKLE.
- tests: cover safe-default rejection, opt-in legacy read, and env toggle.
CHANGELOG: 0.0.9 Security/Fixed entries.
* fix(api): restore route introspection under Starlette >=0.52; fix CI
Two independent CI failures on the 0.0.9 PR.
1. Starlette >=0.52 broke auth/OpenAPI route introspection (4 failures).
Newer Starlette wraps `app.include_router(...)` in an `_IncludedRouter`
object instead of flattening the included APIRoutes into `app.routes`.
jvspatial's auth resolver only scanned top-level APIRoute instances, so
included routes (e.g. the file-storage GET/DELETE endpoints) looked
unregistered -> deny-by-default -> spurious 401s; OpenAPI security
padlocks were also dropped. Added `jvspatial/api/_route_utils.py`
(`iter_api_routes`) which recurses through `_IncludedRouter`
(`.original_router.routes`) and mounts, and routed every route-table
scan through it: endpoint auth resolver (3 sites), OpenAPI security
wiring, dynamic-walker registration, and duplicate-route detection.
Compatible with old and new Starlette. Reproduced and verified fixed
in a python:3.12-slim container (fastapi 0.137 / starlette 0.52);
does not regress the older local stack (fastapi 0.119 / starlette 0.48).
2. Postgres unit tests errored when asyncpg absent (14 failures).
`tests/db/test_postgres_unit.py` now `importorskip("asyncpg")` so it
skips cleanly instead of erroring; CI installs the postgres+pgvector
extras so the unit tests actually run (translator pushdown, pool
auto-tuning, vector encoding). Live-DB integration tests still skip
without JVSPATIAL_POSTGRES_TEST_DSN.
CHANGELOG: 0.0.9 Fixed entry for the Starlette compatibility fix.
* fix(deps): lift Starlette cap to resolve PYSEC-2026-161
Starlette 0.52.x carries PYSEC-2026-161 (fixed in 1.0.1); the previous
`starlette<1.0.0` cap pinned installs to the vulnerable line and failed
the pip-audit CI gate. The cap's original rationale (Starlette 1.0 drops
Router on_startup/on_shutdown) does not apply: jvspatial wires lifecycle
hooks through its own LifecycleManager (FastAPI lifespan), and route
introspection is version-agnostic via _route_utils.iter_api_routes.
Dependency is now `starlette>=0.46.0` (matches FastAPI's own floor, no
upper cap). Fresh installs resolve to a patched Starlette (verified 1.3.1).
Full suite green against Starlette 1.3.1; pip-audit reports no
vulnerabilities with the CI-exact install.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Tharick Jairam <tharick.jairam@v75inc.com>
Adds request-scoped identity map, single-hop find_connected_nodes, auth service reuse, walker batching knobs, and observable db_op coverage. Prepares integral perf audit substrate dependencies. Co-authored-by: Cursor <cursoragent@cursor.com>
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 6 to 21. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](dawidd6/action-download-artifact@v6...v21) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-version: '21' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](actions/download-artifact@v5...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 4 to 5. - [Release notes](https://github.com/peter-evans/create-or-update-comment/releases) - [Commits](peter-evans/create-or-update-comment@v4...v5) --- updated-dependencies: - dependency-name: peter-evans/create-or-update-comment dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
release: 0.0.10 — graph perf, identity map, postgres joins
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](actions/upload-artifact@v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The 0.0.10 single-hop find_connected_nodes fast path had two regressions, both invisible to the suite because JsonDB exposes no find_connected_nodes (tests fell back to the slow edge scan): 1. Limit-before-filter: `limit` was pushed to the DB scan before the Python node-type filter, so node(node=T) / nodes(node=T, limit=n) could return nothing when a non-matching neighbor sorted first. Push `limit` to the DB only when there is no node filter; otherwise apply it after filtering. 2. Subtype resolution under an entity-name collision: rows were deserialized with the base Node class, so when two Node subclasses in one database share an entity name (e.g. an app User and an embedded-agent User), find_subclass_by_name returned the first global match and isinstance-based filtering dropped the valid neighbor. Deserialize using the caller's requested concrete type as the resolution hint. Adds tests/core/test_connected_nodes_fast_path.py, which installs a find_connected_nodes shim so the fast path is actually exercised; both tests fail on 0.0.10 and pass here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 0.0.10 single-hop find_connected_nodes fast path had two regressions, both invisible to the suite because JsonDB exposes no find_connected_nodes (tests fell back to the slow edge scan): 1. Limit-before-filter: `limit` was pushed to the DB scan before the Python node-type filter, so node(node=T) / nodes(node=T, limit=n) could return nothing when a non-matching neighbor sorted first. Push `limit` to the DB only when there is no node filter; otherwise apply it after filtering. 2. Subtype resolution under an entity-name collision: rows were deserialized with the base Node class, so when two Node subclasses in one database share an entity name (e.g. an app User and an embedded-agent User), find_subclass_by_name returned the first global match and isinstance-based filtering dropped the valid neighbor. Deserialize using the caller's requested concrete type as the resolution hint. Adds tests/core/test_connected_nodes_fast_path.py, which installs a find_connected_nodes shim so the fast path is actually exercised; both tests fail on 0.0.10 and pass here. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…paths
_translate_field_clause built (data #>> '{entity}') for every field
including entity/id/tenant_id, even though these are real top-level
Postgres columns (see _bootstrap_collection DDL + postgres.py's own
_pg_field_extract, which already special-cases them correctly for a
different code path). Every typed <NodeClass>.find(...) auto-injects
an entity discriminator filter, so this blinded every find() call to
the entity btree index and forced a full table scan on the shared
node table.
Reproduced against a live Postgres instance: entity = $1 seq-scanned
at ~298ms on a 2.4k-row table; after this fix it's an Index Scan on
node_entity_idx at ~0.03ms.
…lowlist Both are real vars consumed directly in database_configurator.py, but were missing from ALLOWED_ENV_KEYS — every server boot logged "Unknown JVSPATIAL_* env var ... ignored" even though the var worked correctly.
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
…ns/checkout-7 ci(deps): bump actions/checkout from 6 to 7
…olumn-index fix(postgres): translate entity/id/tenant_id to real columns; register observability env keys
The publish-on-push workflow re-uploaded the unchanged 0.0.11 on every merge to main (incl. #26/#27), failing with `400 File already exists`. Two fixes: - Bump __version__ 0.0.11 -> 0.0.12 to release the postgres translator column-index fix + observability env keys. - Add `skip-existing: true` to the pypa/gh-action-pypi-publish step so future merges that don't bump the version are a no-op success instead of a red X. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore(release): bump to 0.0.12; skip-existing on PyPI publish
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
…st noop create_task(..., strict=True) promised that scheduling failures surface to the caller, but strict only guarded one case: the LoggingNoop scheduler resolving in serverless mode. The AWS schedulers themselves never raised — AwsLambdaDeferredTaskScheduler swallowed invoke exceptions and returned a synthetic reference, returned early (warn only) when AWS_LAMBDA_FUNCTION_NAME was unset, and AwsSqsTaskScheduler silently no-op'd when unconfigured. A caller passing strict=True is stating that it has failure handling of its own — a 5xx to the webhook origin, a dedup claim to release — and that a silently-dropped task is data loss. Handing that caller a reference for a task that was never dispatched converts its retry path into exactly that loss: jvagent's WhatsApp deferred-interact webhook returned 200 on real dispatch failures, the wamid stayed claimed, and Meta's retry was dedup-blocked. `strict: bool = False` is added to TaskScheduler.schedule and threaded through dispatch_deferred_task. Under strict: aws_lambda raises on a missing function name and re-raises invoke failures (an EventBridge failure still falls back to direct invoke — a working fallback is not a dispatch failure); aws_sqs raises when unconfigured (send_message failures already propagated); the noop stub raises. Non-strict behaviour is byte-for-byte unchanged, so every existing fire-and-forget caller keeps its semantics. Tests cover both directions per path: strict raises where non-strict returns a synthetic ref, and the success path still returns a reference. 2014 passed, 129 skipped; pre-commit clean on touched files.
`_find_sort_key` looked up sort fields with a flat `record.get(field)`, so a
spec like `sort=[("context.started_at", -1)]` produced `None` for every row
and left results in arbitrary order.
The SQLite and Postgres sort pushdowns (`translate_sort`) and Mongo's native
sort already resolve dotted paths, so the same query ordered correctly on
those backends and silently did not on JsonDB/DynamoDB — or on SQLite and
Postgres whenever the query fell back to the in-memory path.
Resolve dotted paths in `_find_sort_key`; a non-dict segment along the path
yields `None` rather than raising.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nding `finalize_find_results` sorts descending via `reverse=True`, which flipped `_find_sort_key`'s `(value is None, value)` flag along with the values and floated records missing the sort field to the front. Both SQL translators emit NULLS LAST for descending — SQLite via a leading `(col IS NULL)` term that is itself sorted ASC, Postgres via an explicit `DESC NULLS LAST` — and Mongo sorts missing values last. A "newest N" `sort` + `limit` fetch therefore returned real rows on SQLite/Postgres/Mongo and a window of records missing the field on the in-memory path. Invert the None flag for descending sorts so missing values land last in both directions. All missing values share a flag, so None is never compared against a real value. Also correct the comment in `_sqlite_translate.translate_sort`, which asserted the in-memory sort already put NULLs last. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the two properties every adapter has to produce identically whether it pushes the sort into the backend or falls back to `finalize_find_results`: dotted paths resolve into nested documents, and missing values sort last in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`translate_sort` returns None for a field path it cannot safely interpolate (e.g. `context.my-field`), so the ordering has to happen in `finalize_find_results`. The LIMIT was still pushed into the SQL, so the database returned an arbitrary N rows and the in-memory sort ordered that arbitrary subset: `find(sort=..., limit=10)` returned the top 10 of an arbitrary 10 rather than the true top 10. Withhold the LIMIT whenever the sort falls back to memory, then apply it after sorting. `SQLiteDB.find` and `DynamoDB.find` already did this. Also stop re-sorting vector (`$near`) results by the user's `sort`. The comment says the vector ORDER BY wins when both are present, but forcing `sort_sql = None` for vector queries meant the in-memory branch fired and discarded the distance ordering. Applies to both `PostgresDB.find` and the verbatim copy in `PostgresTransaction.find`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…value tail
Two defects in `GraphContext.find_page` keyset pagination.
The cursor payload was minted with a flat `last.get(primary_field)`. For a
dotted sort field the value is nested, so every cursor encoded `sort: None`
and the next page compared against it — raising `TypeError: '>' not supported
between instances of 'int' and 'NoneType'` out of `QueryEngine` on JsonDB
rather than paging. Mint the cursor with `resolve_sort_value`, the same path
walk the adapters and `finalize_find_results` use.
Records missing the sort field sort last in both directions, but the keyset
filter `{field: {"$lt": value}}` can never match a record that has no value,
so iteration stopped at the last record that had one and the trailing run was
unreachable. Add a `{field: None}` branch — which matches both an explicit
null and a missing key — and, when the cursor itself was minted inside that
run, walk it by `id` alone.
Extract the path walk from `_find_sort_key` into `resolve_sort_value` and
export it so cursor minting and adapter sorting cannot drift apart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ObjectPager.get_page` fetched a slice with a DB-side `sort` + `limit`, then
re-sorted that page in Python with
`item.get("context", {}).get(order_by, 0)`. A record missing `order_by`
became `0` and sorted among the real values, while the slice that produced
the page had already placed it in the trailing missing-value run. The two
orderings disagree, so a record could appear on two pages or on none.
The blanket `contextlib.suppress(KeyError, TypeError)` around it also left a
page silently in DB order whenever the key raised.
Route the safety net through `finalize_find_results` instead. It is now a
genuine no-op when the backend honored the sort, and matches the nulls-last
contract when it did not.
`test_get_page_with_ordering` was passing on the in-Python re-sort alone —
its mock ignored `sort` and `limit` entirely, so it could not have caught
this. Switch it to `mock_find_respecting_limit`, which the keyset tests in
the same file already use.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ract Three docstrings still said "NULLs sort last for ascending, first for descending, mirroring finalize_find_results" — the opposite of what the code emits and of the contract: the module docstring and `translate_sort` docstring in `_sqlite_translate.py`, and `translate_sort` in `_postgres_translate.py`. Only the inline comment was corrected when the in-memory ordering was fixed. Move the `find` sort contract from SPEC §4.2 (capability flags) to §4.1, beside the `Database` method table it governs, and extend it with the rule that `limit` must not be pushed down when the sort is not, plus a Known divergences table: MongoDB's native `cursor.sort()` places missing values first on ascending sorts (documented, not normalized — compensating needs an aggregation pipeline on every find), array-index path segments, and heterogeneous value types. Document the contract on `Database.find` for adapter authors, and correct `Database.find_iter`, which claimed a composite `(sort_value, id)` cursor it does not implement — it tracks `id` only, so a non-`id` sort drops records that sort late but carry a lower `id`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`test_find_sort_nulls_last.py` only string-matches the SQL fragments the translators emit, so the two backends the contract binds hardest had no runtime assertion. Add end-to-end `SQLiteDB` coverage that runs the same data through both branches of `find` — ORDER BY pushed into SQL, and the in-memory `finalize_find_results` fallback reached via an unsafe field path — and asserts they agree on values-then-missing ordering in both directions and on the true top N under `limit`. Also normalize a falsy `sort` to `None` in `SQLiteDB.find`. `sort=[]` failed the `sort is None` guard and took the untranslatable-sort branch, loading the whole collection and applying `limit` in memory. Same rows, needless work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…transport Follow-up to the strict-scheduling change, closing the gaps found in review. Blocker: `dispatch_deferred_task` forwarded `strict=` to `sched.schedule()` unconditionally. `TaskScheduler` is a public/stable extension point and `config.task_scheduler` is duck-typed, so every third-party scheduler written against the pre-`strict` signature would have raised `TypeError` on *every* dispatch, non-strict included. The signature is now introspected (cached per class) and `strict` omitted for schedulers that predate it; a `strict=True` dispatch through one raises `TaskSchedulerNotConfiguredError` explaining it cannot honor the guarantee. Remaining silent-failure paths, all now raising under strict: * `NoopOrSyncScheduler` with no executor — what `get_task_scheduler` returns for every non-serverless caller — accepted `strict=True` and dropped the task. * Lambda `invoke` responses were never inspected. An async invoke answers 202 on acceptance, so a non-2xx `StatusCode` or a `FunctionError` was reported as success because boto3 had not raised. * An EventBridge scheduling failure fell back to invoking immediately with `process_at` in the body. Past Lambda's 900s timeout the handler cannot survive until `run_at`, so the task was doomed and the caller still got a success reference. * SQS `send_message` failures propagated while the Lambda transport swallowed them, so the same application code had opposite failure semantics depending on `JVSPATIAL_AWS_DEFERRED_TRANSPORT`. `strict` is now the single switch on both. Replace the ad-hoc `RuntimeError`s with `DeferredTaskError` → `TaskDispatchError` → `TaskSchedulerNotConfiguredError`, so a strict caller can distinguish "retry may succeed" from "this deployment will never dispatch". `DeferredTaskError` also derives from `RuntimeError`, keeping existing handlers working. Emit the one-time no-op diagnostic before the strict raise — a deployment whose callers are all strict never saw the startup error explaining why nothing dispatched. Add coverage for SQS, the logging no-op, the sync fallback, `strict` forwarding through the factory, legacy and `**kwargs` schedulers, and non-raising invoke rejections; the suite was Lambda-only. Rewrite the downstream-consumer narration in docstrings and test data generically per CLAUDE.md, and update `docs/md/serverless-mode.md` and SPEC §11.3, which still described the old narrow meaning of `strict`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pip-audit job was failing on PYSEC-2026-3447 in setuptools 79.0.1 — the version bundled with the runner's Python, which lands in the freeze list the audit consumes. setuptools is neither a declared runtime dependency of jvspatial nor vendored into its wheel, so the finding said nothing about the project's own dependency hygiene while still reddening every PR that touches pyproject.toml or this workflow. Upgrade setuptools alongside pip so the audited environment carries the fixed 83.0.0. Not floored in pyproject's [build-system] requires instead: setuptools 83 requires Python >=3.10 while jvspatial supports >=3.9, so pinning it there would make a source build unresolvable on 3.9. This job pins 3.11, so the upgrade is safe here and keeps the audit reporting on our dependencies. Verified in a clean 3.11 venv: pip-audit --strict reports "No known vulnerabilities found" after the upgrade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ns/setup-python-7 ci(deps): bump actions/setup-python from 6 to 7
feat: add Object.create_if_absent atomic insert
feat(db): PostgresTransaction.find_one_and_update for work-kernel CAS
async_transaction_context opened a backend transaction but kept saving through the pool, so Node.create and connect committed immediately. graph_transaction now uses the transaction handle as the GraphContext database and as the task-local default.
Bind Node writes to one graph transaction
Create a GitHub Release when a new version tag is cut
Release 0.1.0: security review remediation
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 24 to 25. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](dawidd6/action-download-artifact@v24...v25) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-version: '25' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(release): prepare jvspatial 0.1.1 * fix(postgres): serialize bootstrap across instances and bound generated names
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 25 to 27. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](dawidd6/action-download-artifact@v25...v27) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-version: '27' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Contributor
Author
LabelsThe following labels could not be found: Please fix the above issues or remove invalid values from |
Benchmark comparisonThreshold: ±25% (informational, does not block merge)
|
dependabot
Bot
deleted the
dependabot/github_actions/dawidd6/action-download-artifact-27
branch
October 11, 2026 09:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps dawidd6/action-download-artifact from 25 to 27.
Release notes
Sourced from dawidd6/action-download-artifact's releases.
Commits
eab87c9fix: merge unfiltered runs into filtered run search (#432)27e4ae6feat: log skipped workflow runs in debug modeDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)